import qs from "node:querystring";

import {isString} from "@tsed/core";
import {Controller} from "@tsed/di";
import {OnDeserialize} from "@tsed/json-mapper";
import {PlatformContext} from "@tsed/platform-http";
import {PlatformTest} from "@tsed/platform-http/testing";
import {QueryParams} from "@tsed/platform-params";
import {ResponseFilter, type ResponseFilterMethods} from "@tsed/platform-response-filter";
import {PlatformTestSdk} from "@tsed/platform-test-sdk";
import {
  array,
  CollectionOf,
  Default,
  Description,
  For,
  Generics,
  Get,
  getSpec,
  Integer,
  Min,
  MinLength,
  oneOf,
  Property,
  Returns,
  SpecTypes,
  string
} from "@tsed/schema";
import SuperTest from "supertest";

import {PlatformExpress} from "../src/index.js";
import {rootDir, Server} from "./app/Server.js";

class Pageable {
  @Integer()
  @Min(0)
  @Default(0)
  @Description("Page number.")
  page: number = 0;

  @Integer()
  @Min(1)
  @Default(20)
  @Description("Number of objects per page.")
  size: number = 20;

  @For(SpecTypes.JSON, oneOf(string(), array().items(string()).maxItems(2)))
  @For(SpecTypes.OPENAPI, array().items(string()).maxItems(2))
  @OnDeserialize((value: string | string[]) => (isString(value) ? value.split(",") : value))
  @Description("Sorting criteria: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported.")
  sort: string | string[];

  constructor(options: Partial<Pageable>) {
    options.page && (this.page = options.page);
    options.size && (this.size = options.size);
    options.sort && (this.sort = options.sort);
  }

  get offset() {
    return this.page ? this.page * this.limit : 0;
  }

  get limit() {
    return this.size;
  }
}

@Generics("T")
class Pagination<T> extends Pageable {
  @CollectionOf("T")
  data: T[];

  @Integer()
  @MinLength(0)
  totalCount: number = 0;

  constructor({data, totalCount, pageable}: Partial<Pagination<T>> & {pageable: Pageable}) {
    super(pageable);
    data && (this.data = data);
    totalCount && (this.totalCount = totalCount);
  }

  get isPaginated() {
    return this.data.length < this.totalCount;
  }
}

class Product {
  @Property()
  id: string;

  @Property()
  title: string;

  constructor({id, title}: Partial<Product> = {}) {
    id && (this.id = id);
    title && (this.title = title);
  }
}

const utils = PlatformTestSdk.create({
  rootDir,
  adapter: PlatformExpress as any,
  server: Server
});

@Controller("/pageable")
class TestPageableCtrl {
  @Get("/")
  @(Returns(206, Pagination).Of(Product).Title("PaginatedProduct"))
  @(Returns(200, Pagination).Of(Product).Title("PaginatedProduct"))
  get(@QueryParams() pageableOptions: Pageable, @QueryParams("all") all: boolean) {
    return new Pagination<Product>({
      data: [
        new Product({
          id: "100",
          title: "CANON D3000"
        })
      ],
      totalCount: all ? 1 : 100, // just for test
      pageable: pageableOptions
    });
  }
}

@ResponseFilter("application/json")
class PaginationFilter implements ResponseFilterMethods {
  transform(data: unknown, ctx: PlatformContext): any {
    if (ctx.data instanceof Pagination) {
      // get the unserialized data
      if (ctx.data.isPaginated) {
        ctx.response.status(206);
      }
    }

    return data;
  }
}

describe("Pageable", () => {
  let request: SuperTest.Agent;

  beforeAll(
    utils.bootstrap({
      mount: {
        "/rest": [TestPageableCtrl]
      },
      responseFilters: [PaginationFilter],
      swagger: [
        {
          path: "/v2/docs",
          specVersion: "2.0" // by default
        },
        {
          path: "/v3/docs",
          specVersion: "3.0.1"
        }
      ]
    })
  );
  afterAll(() => utils.reset());

  beforeAll(() => {
    request = SuperTest(PlatformTest.callback());
  });

  it("should generate spec", () => {
    const spec = getSpec(TestPageableCtrl, {specType: SpecTypes.OPENAPI});
    expect(spec).toMatchInlineSnapshot(`
      {
        "components": {
          "schemas": {
            "PaginatedProduct": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Pagination",
                },
                {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/Product",
                      },
                      "type": "array",
                    },
                  },
                  "type": "object",
                },
              ],
            },
            "Pagination": {
              "properties": {
                "data": {
                  "items": {},
                  "type": "array",
                },
                "page": {
                  "default": 0,
                  "description": "Page number.",
                  "minimum": 0,
                  "multipleOf": 1,
                  "type": "integer",
                },
                "size": {
                  "default": 20,
                  "description": "Number of objects per page.",
                  "minimum": 1,
                  "multipleOf": 1,
                  "type": "integer",
                },
                "sort": {
                  "description": "Sorting criteria: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported.",
                  "items": {
                    "type": "string",
                  },
                  "maxItems": 2,
                  "type": "array",
                },
                "totalCount": {
                  "minLength": 0,
                  "multipleOf": 1,
                  "type": "integer",
                },
              },
              "type": "object",
            },
            "Product": {
              "properties": {
                "id": {
                  "type": "string",
                },
                "title": {
                  "type": "string",
                },
              },
              "type": "object",
            },
          },
        },
        "paths": {
          "/pageable": {
            "get": {
              "operationId": "testPageableCtrlGet",
              "parameters": [
                {
                  "description": "Page number.",
                  "in": "query",
                  "name": "page",
                  "required": false,
                  "schema": {
                    "default": 0,
                    "minimum": 0,
                    "multipleOf": 1,
                    "type": "integer",
                  },
                },
                {
                  "description": "Number of objects per page.",
                  "in": "query",
                  "name": "size",
                  "required": false,
                  "schema": {
                    "default": 20,
                    "minimum": 1,
                    "multipleOf": 1,
                    "type": "integer",
                  },
                },
                {
                  "description": "Sorting criteria: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported.",
                  "in": "query",
                  "name": "sort",
                  "required": false,
                  "schema": {
                    "items": {
                      "type": "string",
                    },
                    "maxItems": 2,
                    "type": "array",
                  },
                },
                {
                  "in": "query",
                  "name": "all",
                  "required": false,
                  "schema": {
                    "type": "boolean",
                  },
                },
              ],
              "responses": {
                "200": {
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/PaginatedProduct",
                      },
                    },
                  },
                  "description": "Success",
                },
                "206": {
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/PaginatedProduct",
                      },
                    },
                  },
                  "description": "Partial Content",
                },
              },
              "tags": [
                "TestPageableCtrl",
              ],
            },
          },
        },
        "tags": [
          {
            "name": "TestPageableCtrl",
          },
        ],
      }
    `);
  });

  it("should get paginated products with a status 206 (with array)", async () => {
    const options = {
      page: 1,
      size: 10,
      sort: ["field", "asc"]
    };

    const {body} = await request.get("/rest/pageable?" + qs.stringify(options)).expect(206);

    expect(body).toEqual({
      data: [
        {
          id: "100",
          title: "CANON D3000"
        }
      ],
      page: 1,
      size: 10,
      sort: ["field", "asc"],
      totalCount: 100
    });
  });
  it("should get paginated products with a status 206 (with string)", async () => {
    const options = {
      page: 1,
      size: 10,
      sort: "field,asc"
    };

    const {body} = await request.get("/rest/pageable?" + qs.stringify(options)).expect(206);

    expect(body).toEqual({
      data: [
        {
          id: "100",
          title: "CANON D3000"
        }
      ],
      page: 1,
      size: 10,
      sort: ["field", "asc"],
      totalCount: 100
    });
  });

  it("should get all products with a status 206", async () => {
    const options = {
      all: true
    };

    const {body} = await request.get("/rest/pageable?" + qs.stringify(options)).expect(200);

    expect(body).toEqual({
      data: [{id: "100", title: "CANON D3000"}],
      totalCount: 1,
      page: 0,
      size: 20
    });
  });

  it("should not return a bad request when sort is not given", async () => {
    const options = {
      page: 1,
      size: 10
    };

    const {body} = await request.get("/rest/pageable?" + qs.stringify(options)).expect(206);

    expect(body).toEqual({
      data: [
        {
          id: "100",
          title: "CANON D3000"
        }
      ],
      page: 1,
      size: 10,
      totalCount: 100
    });
  });

  it("should apply default pagination parameters", async () => {
    const options = {};

    const {body} = await request.get("/rest/pageable?" + qs.stringify(options)).expect(206);

    expect(body).toEqual({
      data: [{id: "100", title: "CANON D3000"}],
      totalCount: 100,
      page: 0,
      size: 20
    });
  });

  it("should throw bad request when options isn't correct", async () => {
    const options = {
      page: -1
    };

    const {body} = await request.get("/rest/pageable?" + qs.stringify(options)).expect(400);

    expect(body).toEqual({
      errors: [
        {
          data: -1,
          requestPath: "query",
          dataPath: ".page",
          instancePath: "/page",
          keyword: "minimum",
          message: "must be >= 0",
          modelName: "Pageable",
          params: {
            comparison: ">=",
            limit: 0
          },
          schemaPath: "#/properties/page/minimum"
        }
      ],
      message: 'Bad request on parameter "request.query".\nPageable.page must be >= 0. Given value: -1',
      name: "AJV_VALIDATION_ERROR",
      status: 400
    });
  });
});
